﻿/* **********************************************************************************
 * Copyright (c) 2011 John Hughes
 *
 * Elve Touch is licenced under the Microsoft Reciprocal License (Ms-RL).
 *
 * Project Website: http://elvetouch.codeplex.com/
 * **********************************************************************************/

using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
using Prophecy.Communication;

namespace ElveTouch
{
    public interface IBinaryTcpPayload
    {
        byte PayloadType { get; }
        byte[] ToByteArray();
    }

    public class MessageReceivedEventArgs : EventArgs
    {
        public UInt16 SequenceNumber;
        public Byte PayloadType;
        public byte[] Payload;

        public MessageReceivedEventArgs(UInt16 sequenceNumber, Byte payloadType, Byte[] payload)
        {
            SequenceNumber = sequenceNumber;
            PayloadType = payloadType;
            Payload = payload;
        }
    }

    public class BinaryTcpCommunication : TcpCommunication
    {
        public event EventHandler<MessageReceivedEventArgs> MessageReceived;

        public BinaryTcpCommunication(Socket socket)
            : base(socket)
        {
            base.Delimiter = null; // set to null to disable the ReceivedDelimitedString
            base.CurrentEncoding = null;
            base.ReceivedBytes += new EventHandler<ReceivedBytesEventArgs>(BinaryTcpCommunication_ReceivedBytes);
        }

        public BinaryTcpCommunication(string hostName, int port)
            : this(hostName, port, null)
        {
        }

        public BinaryTcpCommunication(string hostName, int port, IPEndPoint localEndPoint)
            : base(hostName, port, localEndPoint)
        {
            base.Delimiter = null; // set to null to disable the ReceivedDelimitedString
            base.CurrentEncoding = null;
            base.ReadBufferEnabled = true;
            base.MaximumReadBufferSize = 1024 * 1024 * 50; // (50MB) this needs to be large enough for an entire screen shot or any graphic files used.
            base.ReceivedBytes += new EventHandler<ReceivedBytesEventArgs>(BinaryTcpCommunication_ReceivedBytes);
            base.ReadBufferOverflow += new EventHandler(BinaryTcpCommunication_ReadBufferOverflow);
        }


        RendererHeader _rendererHeader;
        bool _haveRendererHeader = false;
        void BinaryTcpCommunication_ReceivedBytes(object sender, ReceivedBytesEventArgs e)
        {
            try
            {
                while (true)
                {
                    //******************************************************************
                    // Find the start of the next packet.
                    //******************************************************************
                    if (_haveRendererHeader == false)
                    {
                        _haveRendererHeader = getNextValidRendererHeader(out _rendererHeader);
                        if (_haveRendererHeader == false)
                            return; // no header is available yet
                    }

                    //******************************************************************
                    // Read and process the payload.
                    //******************************************************************
                    if (ReadBuffer.Length < _rendererHeader.PayloadLength)
                        return; // the full payload has not been received yet
                    
                    //******************************************************************
                    // Read Payload
                    //******************************************************************
                    byte[] payload = ReadBuffer.ReadBytes((int)_rendererHeader.PayloadLength);

                    //******************************************************************
                    // MessageReceived Event
                    //******************************************************************
                    MessageReceivedEventArgs e2 = new MessageReceivedEventArgs(_rendererHeader.SequenceNumber, _rendererHeader.PayloadType, payload);

                    OnMessageReceived(e2);

                    _haveRendererHeader = false;
                }
            }
            catch (Exception ex)
            {
                Logger.Error("An error occured in BinaryTcpCommunication_ReceivedBytes().", ex);
            }
        }


        bool getNextValidRendererHeader(out RendererHeader header)
        {
            int HL = RendererHeader.RENDERERHEADERBYTELENGTH;

            while (true)
            {
                if (ReadBuffer.Length < HL)
                    break;

                long bolPos = ReadBuffer.IndexOf(new byte[] { 0 }, 0);
                if (bolPos < 0 || ReadBuffer.Length < bolPos + HL)
                    break;

                RendererHeader readHeader = new RendererHeader(ReadBuffer);
                if (readHeader.IsValid)
                {
                    header = readHeader;
                    return true;
                }
            }

            header = new RendererHeader();
            return false;
        }

        void BinaryTcpCommunication_ReadBufferOverflow(object sender, EventArgs e)
        {
            // TODO: if we get here then we have a major problem. We should close the connection and reconnect.
        }




        public void SendMessage(IBinaryTcpPayload payload)
        {
            SendMessage(payload.PayloadType, payload.ToByteArray());
        }

        private static UInt16 _nextSequenceNumber = 0;
        private readonly object send_lock = new object();
        public void SendMessage(byte payloadType, byte[] payload)
        {
            lock (send_lock)
            {
                try
                {
                    if (payload == null)
                        payload = new byte[0];

                    // Calculate the payload checksum.
                    Byte payloadChecksum = SystemUtility.CalculateByteChecksum(payload);

                    RendererHeader header = new RendererHeader(_nextSequenceNumber, payloadType, (UInt32)payload.Length, payloadChecksum);

                    byte[] data = header.ToByteArray(payload);

                    // Send the data.
                    base.Send(data);

                    // Increment the sequence number.
                    if (_nextSequenceNumber == UInt16.MaxValue)
                        _nextSequenceNumber = 0;
                    else
                        _nextSequenceNumber++;

                }
                catch (Exception ex)
                {
                    //System.Diagnostics.Debugger.Break();
                }
            }
        }

        protected virtual void OnMessageReceived(object e)
        {
            MessageReceivedEventArgs e2 = (MessageReceivedEventArgs)e;
            if (MessageReceived != null)
            {
                try
                {
                    MessageReceived(this, e2);
                }
                catch
                {
                    // just catch the error in the user code and continue.
                }
            }
        }

        public override void Dispose()
        {
            base.ReceivedBytes -= new EventHandler<ReceivedBytesEventArgs>(BinaryTcpCommunication_ReceivedBytes);

            base.Dispose();
        }




    }

    public struct RendererHeader
    {
        public const int RENDERERHEADERBYTELENGTH = 11;

        public Byte BeginningOfHeader; // 0;
        public UInt16 SequenceNumber; // a rolling incremental number (circular)
        public Byte PayloadType;  // this is like a command id and therefore indicates the format of the payload.
        public UInt32 PayloadLength;
        public Byte PayloadChecksum; // the checksum of just the payload.
        public Byte HeaderChecksum; // includes all of header except this checksum and EndOfHeader
        public Byte EndOfHeader; // 13;

        public RendererHeader(UInt16 sequenceNumber, Byte payloadType, UInt32 payloadLength, Byte payloadChecksum)
        {
            this.BeginningOfHeader = 0; // always 0.
            this.SequenceNumber = sequenceNumber;
            this.PayloadType = payloadType;
            this.PayloadLength = payloadLength;
            this.PayloadChecksum = payloadChecksum;
            this.HeaderChecksum = 0;
            this.EndOfHeader = 13; // always 13.

            this.HeaderChecksum = calculateHeaderChecksum();
        }

        public RendererHeader(Stream ms)
        {
            using (BinaryStreamReader s = new BinaryStreamReader(ms))
            {
                this.BeginningOfHeader = s.ReadByte();
                this.SequenceNumber = s.ReadUInt16();
                this.PayloadType = s.ReadByte();
                this.PayloadLength = s.ReadUInt32();
                this.PayloadChecksum = s.ReadByte();
                this.HeaderChecksum = s.ReadByte();
                this.EndOfHeader = s.ReadByte();
            }
        }

        public bool IsValid
        {
            get
            {
                if (BeginningOfHeader != 0 ||
                    EndOfHeader != 13 ||
                    calculateHeaderChecksum() != HeaderChecksum)
                    return false;
                else 
                    return true;
            }
        }

        private Byte calculateHeaderChecksum()
        {
            byte[] b = this.ToByteArray();
            return SystemUtility.CalculateByteChecksum(b, 0, 9); // everything except the checksum byte and end of record byte.
        }

        public Byte[] ToByteArray()
        {
            using (BinaryStreamWriter s = new BinaryStreamWriter())
            {
                s.Write(BeginningOfHeader);
                s.Write(SequenceNumber);
                s.Write(PayloadType);
                s.Write(PayloadLength);
                s.Write(PayloadChecksum);
                s.Write(HeaderChecksum);
                s.Write(EndOfHeader);

                return s.ToArray();
            }
        }

        public Byte[] ToByteArray(bool allocatePayloadBytes)
        {
            byte[] headerBytes = this.ToByteArray();

            byte[] data = new byte[headerBytes.Length + this.PayloadLength];

            Array.Copy(headerBytes, data, headerBytes.Length);

            return data;
        }

        public Byte[] ToByteArray(byte[] payloadToAppendToHeader)
        {
            byte[] headerBytes = this.ToByteArray();

            byte[] data = new byte[headerBytes.Length + this.PayloadLength];

            Array.Copy(headerBytes, data, headerBytes.Length);
            Array.Copy(payloadToAppendToHeader, 0, data, headerBytes.Length, (int)this.PayloadLength);
            
            return data;
        }
    }
}
